//
//  FlacFuseAudioObject.m
//  FlacFuse
//
//  Created by Fabián Cañas on 9/13/10.
//  Copyright (c) 2010 University of Colorado, Boulder. All rights reserved.
//

#import "FlacFuseAudioObject.h"

#import <dispatch/dispatch.h>


#pragma mark --
#pragma mark -- FLAC Utility Functions
static FLAC__bool write_little_endian_uint32(FILE *f, FLAC__uint32 x)
{
	return
    fputc(x, f) != EOF &&
    fputc(x >> 8, f) != EOF &&
    fputc(x >> 16, f) != EOF &&
    fputc(x >> 24, f) != EOF
	;
}

static FLAC__bool write_little_endian_uint16(FILE *f, FLAC__uint16 x)
{
	return
    fputc(x, f) != EOF &&
    fputc(x >> 8, f) != EOF
	;
}

static FLAC__bool write_little_endian_int16(FILE *f, FLAC__int16 x)
{
	return write_little_endian_uint16(f, (FLAC__uint16)x);
}

#pragma mark --
#pragma mark -- Dummy FLAC Callbacks
FLAC__StreamDecoderWriteStatus tuneFusionDummyWriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data){
    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

void tuneFusionDummyMDCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data){
    *((unsigned long long *)client_data) = (metadata->data.stream_info.total_samples * metadata->data.stream_info.channels * (metadata->data.stream_info.bits_per_sample>>3)) + 36;
    return;
}


void tuneFusionErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data){
    NSLog(@"Error callback in FLAC decode.");
    return;
}

#pragma mark --
#pragma mark -- Decoding FLAC Callbacks
FLAC__StreamDecoderWriteStatus tuneFusionWriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data) {
    FlacFuseAudioObject *ao = (FlacFuseAudioObject *)client_data;
    
    //FILE *f = fopen([[ao dataFileLocation] cStringUsingEncoding:NSUTF8StringEncoding], "a");
    
    FILE *f = ao.dataFile;
    const FLAC__uint32 total_size = (FLAC__uint32)(ao.total_samples * ao.channels * (ao.bps/8));
	size_t i;
    
    
    if(frame->header.number.sample_number == 0) {
		if(
           fwrite("RIFF", 1, 4, f) < 4 ||
           !write_little_endian_uint32(f, total_size + 36) ||
           fwrite("WAVEfmt ", 1, 8, f) < 8 ||
           !write_little_endian_uint32(f, 16) ||
           !write_little_endian_uint16(f, 1) ||
           !write_little_endian_uint16(f, (FLAC__uint16)ao.channels) ||
           !write_little_endian_uint32(f, ao.sample_rate) ||
           !write_little_endian_uint32(f, ao.sample_rate * ao.channels * (ao.bps>>3)) ||
           !write_little_endian_uint16(f, (FLAC__uint16)(ao.channels * (ao.bps>>3))) || /* block align */
           !write_little_endian_uint16(f, (FLAC__uint16)ao.bps) ||
           fwrite("data", 1, 4, f) < 4 ||
           !write_little_endian_uint32(f, total_size)
           ) {
			fprintf(stderr, "ERROR: write error in header\n");
			return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
		}
	}
    
    /* write decoded PCM samples */
	for(i = 0; i < frame->header.blocksize; i++) {
		if(
           !write_little_endian_int16(f, (FLAC__int16)buffer[0][i]) ||  /* left channel */
           !write_little_endian_int16(f, (FLAC__int16)buffer[1][i])     /* right channel */
           ) {
			fprintf(stderr, "ERROR: write error in data\n");
			return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
		}
	}
    
    //fclose(f);
    
    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
    //FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}

void tuneFusionMDCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data){
    FlacFuseAudioObject *ao = (FlacFuseAudioObject *)client_data;
    
    if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
        /* save for later */
        ao.total_samples = metadata->data.stream_info.total_samples;
        ao.sample_rate = metadata->data.stream_info.sample_rate;
        ao.channels = metadata->data.stream_info.channels;
        ao.bps = metadata->data.stream_info.bits_per_sample;
    }
    return;
}

#pragma mark --
#pragma mark -- Class Implementation
@implementation FlacFuseAudioObject

@synthesize isFinishedTranscoding, lastAccess, sourcePath, fileProperties, dataFileLocation, dataFile, fildes, streamDecoder, bps, channels, sample_rate, total_samples;

- (id)initWithSourcePath:(NSString *)path {
    if ((self = [super init])) {
        
        // Initialize to given source path, isn't finished transcoding, and last accessed when created.
        self.sourcePath = path;
        self.isFinishedTranscoding = FALSE;
        self.lastAccess = [NSDate date];
        
        // Stream Decoder for metadata (this part's fast)
        // We use this to construct a dictionary of the new WAV file's attributes
        unsigned long long wavSize = 0;
        self.streamDecoder = FLAC__stream_decoder_new();
        if (streamDecoder!=NULL) {
            FILE *flacFile = fopen([sourcePath UTF8String], "r");
            FLAC__StreamDecoderInitStatus initStatus = FLAC__stream_decoder_init_FILE(streamDecoder,
                                                                                      flacFile,
                                                                                      &tuneFusionDummyWriteCallback,
                                                                                      &tuneFusionDummyMDCallback,
                                                                                      &tuneFusionErrorCallback,
                                                                                      &wavSize);
            
            NSAssert(!(BOOL)initStatus, @"Stream decoder for MetaData could not be initialized.");
            FLAC__stream_decoder_process_until_end_of_metadata(streamDecoder);
            FLAC__stream_decoder_delete(streamDecoder);
            fclose(flacFile);
        }
        // Now we know the future size of the WAV file, so we rebuild the file properties dictionary with the new size.
        self.fileProperties = [NSMutableDictionary dictionaryWithDictionary:[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]];
        [(NSMutableDictionary *)(self.fileProperties) setObject:[NSNumber numberWithUnsignedLongLong:wavSize] forKey:NSFileSize];
        
        
        // Setup temporary file where transcoded data goes:
        //
        // First determine a probably-unique filename based on self's address
        self.dataFileLocation = [@"TuneFusion_" stringByAppendingString:[[NSNumber numberWithInt:(int)self] stringValue]];
        self.dataFileLocation = [NSTemporaryDirectory() stringByAppendingPathComponent:dataFileLocation];
        // Actually make the temporary file
        NSFileManager *filemgr;
        filemgr = [NSFileManager defaultManager];
        [filemgr createFileAtPath:self.dataFileLocation contents:[NSData data] attributes:[NSDictionary dictionary]];
        self.dataFile = fopen([self.dataFileLocation cStringUsingEncoding:NSUTF8StringEncoding],"a");
        NSLog(@"temp file: %@",self.dataFileLocation);
        
        // and fildes begins as invalid, indicating that it's not open.
        self.fildes = -1;
    }
    return self;
}

- (NSString *) dataFileLocation {
    /*   If someone's asking for our temporary file's location, they must be reading the data, and so
     *   it must still be relevant. So we should keep our data fresh.
     */
    self.lastAccess = [NSDate date];
    return dataFileLocation;
}

-(BOOL) open {
    BOOL t;
    @synchronized(self.dataFileLocation){
        t = !self.isFinishedTranscoding;
    }
    if (t){
        FlacFuseAudioObject *ao = self;
        dispatch_queue_t dispatchQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long) NULL);
        dispatch_async(dispatchQ, ^{
            @synchronized(self.dataFileLocation){
                FLAC__StreamDecoder *localStreamDecoder = FLAC__stream_decoder_new();
                if (streamDecoder!=NULL) {
                    FILE *flacFile = fopen([ao.sourcePath UTF8String], "r");
                    NSLog(@"working with file: %@",self.sourcePath);
                    FLAC__StreamDecoderInitStatus initStatus = FLAC__stream_decoder_init_FILE(localStreamDecoder,
                                                                                              flacFile,
                                                                                              &tuneFusionWriteCallback,
                                                                                              &tuneFusionMDCallback,
                                                                                              &tuneFusionErrorCallback,
                                                                                              (void*)ao);
                    
                    if (initStatus){
                        NSLog(@"could not build stream decoder");
                    }
                    
                    FLAC__stream_decoder_process_until_end_of_stream(localStreamDecoder)?ao.isFinishedTranscoding=YES:NSLog(@"not the end.");
                    FLAC__stream_decoder_delete(localStreamDecoder);
                    fclose(flacFile);
                    fclose(ao.dataFile);
                    ao.isFinishedTranscoding = TRUE;
                }
            }
        });
    }
    
    // I'm pretty sure this needs to be locked with a mutex.
    @synchronized(self.dataFileLocation){
        self.fildes = open([self.dataFileLocation UTF8String], O_RDWR);
        NSLog(@"Opening file: %@ with descriptor: %d", self.dataFileLocation, self.fildes);
        return (self.fildes!=-1);
    }
}

-(int) readToBuffer:(char *)buffer size:(size_t)size offset:(off_t)offset error:(NSError **)error {
    // Almost positive this needs to be locked with a mutex.
    @synchronized(self.dataFileLocation){
        NSLog(@"reading from buffer...");
        
        int r = pread(self.fildes, buffer, size, offset);
        NSLog(@"read bytes: %d",r);
        
        return r;
    }
}

-(int) writeFromBuffer:(char *)buffer size:(size_t)size offset:(off_t)offset error:(NSError **)error {
    @synchronized(self.dataFileLocation){
        NSLog(@"reading from buffer...");
        
        int r = pwrite(self.fildes, buffer, size, offset);
        NSLog(@"read bytes: %d",r);
        
        return r;
    }
}

-(void) close {
    // Probably doesn't need ot be locked, but I'm playing it safe?
    @synchronized(self.dataFileLocation){
        NSLog(@"Closing file: %@", self.dataFileLocation);
        close(self.fildes);
    }
}


- (void)dealloc {
    // Clean-up retained properties
    self.sourcePath = nil;
    self.lastAccess = nil;
    self.dataFileLocation = nil;
    
    // Remove temporary file
    NSFileManager *filemgr;
    filemgr = [NSFileManager defaultManager];
    [filemgr removeItemAtPath: self.dataFileLocation error: NULL];
    
    [super dealloc];
}

@end
